<?php
/*--------------------------------------------------------------------------------------------------
class templateFeederMagic

string feed($template,$source_array):
injects values from a multidimensionnal, associative array into a template string
works by searching commands like __command__ and processes them

supported commands :
__A_CONSTANT__
replaces allcaps+underscore string by the constant if defined, else deletes it

__{path_to_data}__
searches for corresponding data in $array_source and dumps its value, else deletes it. See arraypath for {path} format.

__if{path}#{id}__ , if-notnull{path}#{id}__
processes the block until __end#{id}__ if value exists and is not null (or false)

__if-not{path}#{id}__
processes the block until __end#{id}__ if value is null, false, 0, empty

__if{path}equal{path2}#{id}__ , __if{path}equal({value})#{id}__
processes the block if litteral value of left operand equals litteral value of right operand

__if{path}notequal{path2}#{id}__ , __if{path}notequal({value})#{id}__
processes the block if litteral value of left operand DO NOT equals litteral value of right operand

__switch{path}#{id}__ , __case({value})#{id2}__ , case({path2})#{id3}__ , __default#{id4}__
processes the 'case' block corresponding to path value, or the 'default' block if provided

for-in
__for-in({start},{end},{increment})#{id}__
processes n times the block, incrementing a counter from start to end with increment : all 3 parameters can be signed integers.
the counter can be accessed from template at __[template][{id}]__
n >> [template][id]

foreach
__for-each{path}#{id}__
processes the block for each key of array at {path}, with relative context

noparse
__noparse#{id}__
ignore all commands until __end#{id}__

injection de données dans un modèle
repère des structures de la forme __(commande)__ et opère les remplacements demandés


requires class datatree

http://code.fredbird.org
file version 2005-09-29 16:41:34

2005-09-23 13:48:58
	complete refactoring in order to parse the template string only once
	
2005-09-29 16:41:38
	debbugged unwanted wasting of chunk after commands __~__

commands to re-implement
	__for-in({start},{end},{increment})#{id}__
--------------------------------------------------------------------------------------------------*/

class templateFeederMagic {
/*----------------------------------------------------------------------------------------------
                                                                           parametres par defaut
----------------------------------------------------------------------------------------------*/
	var $default_parameters=array();
	var $data=array(); // données de travail
	/* attention ! le nombre de parenthèses de ereg_command a un impact sur la creation d'autres regex
	voir feedblock(), getblock() etc
	*/
	//var $ereg_command='[][)(~a-zA-Z0-9.,/@+*_-]+';
	//var $ereg_command='([^_]|(._.))+';
	//var $ereg_chars_command="[][)(~a-zA-Z0-9.,/@+*_-]";
	//var $ereg_command='[^_ #]+(_?[^_ #]+)?'; // autorise au plus un _ isolé au sein de la commande, et ni espace ni #
	var $ereg_command='[^_ #]+(_?[^_ #]+)*'; // autorise au plus un _ consécutif au sein de la commande, et ni espace ni #
	var $ereg_constant='[A-Z]+[A-Z_]*';
	var $ereg_path=NULL;
	var $ereg_value='\([^()]+\)';
	//var $ereg_wholecommand="__";
	//var $ereg_static_value='[:alnum:]+';
	var $ereg_static_value='[a-zA-Z0-9]+!?';
	//var $ereg_chars=
	var $ereg_id='#[a-zA-Z0-9]+';
	var $verbose=false;
	var $maxCommands=100;
	var $switchValues=array(); // "pile" des valeurs en cours d'evaluation des commandes switch
	var $waste=array(); // tableau collecteur des dechets de regex
	var $maxChunks=500;
/*----------------------------------------------------------------------------------------------
                                                                           constructeur eventuel
----------------------------------------------------------------------------------------------*/
	/*function kap_(& $controller) {
		// appel du constructeur parent
		$const=get_parent_class($this);
		parent::$const($controller);
		// suite
	}*/
/*----------------------------------------------------------------------------------------------
                                                                              methodes publiques
a lier dans components/controller.kap-core.php et api.kap-core.php
----------------------------------------------------------------------------------------------*/
	// injecte les données de data dans la chaine template
	function feed($template,$data=NULL,$verbose=false) {
		if (!is_string($template)) {
			trigger_error("templatefeedermagic: template is not typed string but typed ".gettype($template),E_USER_WARNING);
			print_r($template);
			return false;
		}
		//echo "templatefeeder: feeding ".affvar($template,'template')." with ".affvar($data,'data');
		// le template est il un fichier ?
		if (strlen($template)<255 and file_exists($template) and $lines=file($template)) {
			// echo '<p>Parsing '.$template.'</p>';
			$template=join('',$lines);
		}
		// echo wakaffvar($data,'data');
		// si data est deja un datatree, on le référence simplement
		if (is_object($data) and get_class($data)=='datatree') {
			$this->data=& $data;
			// $this->data->setSyntax('brackets');
			//echo "<p>imported datatree ".kapaffvar($this->data->get())."</p>";
		// sinon on crée un datatree
		} else if (is_array($data)) {
			$this->data=&new dataTree();
			$this->data->loadData($data);
			// $this->data->setSyntax('brackets');
			//echo "<p>invoked datatree ".kapaffvar($this->data->get())."</p>";
		// on crée un datatree vide
		} else if (is_null($data)) {
			$this->data=&new datatree();
		} else {
			$type=gettype($data);
			trigger_error("templatefeedermagic->feed(template,data): data must be either null, an array or a datatree object. Type was $type",E_USER_WARNING);
			/*echo "<pre>";
			print_r($data);
			echo "</pre>";*/
			if (function_exists('wakaffvar')) echo wakaffvar($data);
			return false;
		}
		$this->data->setSyntax('brackets');
		// echo wakaffvar($this->data->get(),'data for template injection');
		$this->verbose=$verbose;
		// construction d'expressions régulières
		// import de l'expression réguliere de chemin de datatree
		$this->ereg_path=$this->data->makePathEreg();// echo "ereg_path = ".$this->ereg_path;
		/*$this->ereg_commands='('.$this->ereg_constant.')'
		.'|('.$this->data->makePathEreg().')';*/
		//return $this->feedBlock($template);
		return $this->feedString($template);
	}
/*----------------------------------------------------------------------------------------------
                                                                                méthodes privées
----------------------------------------------------------------------------------------------*/
	/* traitement linéaire d'un template
	, on parcourt une seule fois la chaine en distinguant la partie traitée de la partie à traiter
	*/
	function & feedString(& $template,$context=NULL) {
		// indication du contexte
		$this->data->setContext($context);
		$parsed=''; $toparse=$template;
		// parcours de la chaine restant à traiter
		$n=0;
		/* toute chaine encadrée par un double _, ne contenant pas d'espace et au plus un _ consécutif
		attention ! cette syntaxe nécessite d'ignorer toute entrée suivant la commande
		*/
		$ereg='/(__[^[:space:]_](_?[^[:space:]_]+)*__)/';
		/* decoupage du template en tableau contenant les commandes et les non-commandes
		*/
		$chunks=preg_split($ereg,$toparse,-1,PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
		// echo wakaffvar($chunks,'chunks');
		// echo $template.' => '.wakaffvar($test);
		$parsed=$this->treatChunks($chunks,$context);
		//echo wakxmlentities($parsed);
		// wakLog("wasted ".wakaffvar($this->waste,'waste',1000));
		return $parsed;
	}
	
	function treatChunks(&$chunks,$context=NULL) {
		if (!is_array($chunks)) return false;
		$parsed='';
		// expression reguliere de definition de commande
		$ereg2='__('.$this->ereg_command.')('.$this->ereg_id.')?__';
		$n=0; $max=$this->maxChunks;
		while ($chunk=array_shift($chunks)) {
			$n++; if ($n>$max) {
				trigger_error("templatefeedermagic: more than $max chunks to treat.",E_USER_WARNING);
				break;
			}
			// echo '<p>'.wakaffvar($chunk).'</p>';
			// identification des commandes
			if (ereg($ereg2,$chunk,$found)) {
				// il s'agit d'une commande
				$command=&$found[1];
				$id=substr($found[3],1);
				// on ignore l'item suivant (parasite du à $ereg)
				// sauf cas particulier, commande d'1 caractere
				//if (strlen($command)<=1)
				if ($command!='~')
					$this->waste[]=array_shift($chunks).' in treatChunks'; //echo '<p>'.wakaffvar($waste,'waste').'</p>';
				//echo '<p>'.wakaffvar($chunk,'commande').'</p>';
				//echo '<p>'.wakaffvar($found,$chunk).'</p>';
				//echo '<p>commande '.$command.' id='.$id.'</p>';
				if ($id) {
					// commande de type bloc, recherche de la fin du bloc
					$parsed.=$this->treatBlock($chunks,$command,$id,$context);
				} else {
					$replaced=$this->treatSimpleCommand($command);
					// echo '<p>'.wakaffvar($replaced,$command).'</p>';
					$parsed.=$replaced;
				}
				
			} else {
				// echo '<p>'.wakaffvar($chunk,'non-commande').'</p>';
				$parsed.=$chunk;
			}
		}
		return $parsed;
	}
	
	// traite une commande atomique (non-bloc)
	function treatSimpleCommand($command,$context=NULL) {
		// analyse du type de commande
		//$ereg_constant=
		switch (true) {
			case ereg($this->ereg_constant,$command):
				$value=$this->getConstant($command);
				break;
			default:
				$value=$this->getPathValue($command,$context);
		}
		// echo "<p>treated ".wakaffvar($value,$command)." (context $context)</p>\n";
		return $value;
	}
	/* traitement d'un bloc de chunks
	*/
	function treatBlock(&$chunks,$command,$id,$context) {
		$parsed='';
		if (!$newchunks=$this->extractChunksBlock($chunks,$id)) {
			return false;
		}
		switch (true) {
			// attention ! l'ordre des case est important
			case ereg('^foreach(.*)$',$command,$found):
			case ereg('^for-each(.*)$',$command,$found):
				$path=$found[1];
				return $this->treatForeach($path,$newchunks,$context);
				break;
			case ereg('^switch(.+)$',$command,$found):
				$pointer=$found[1];
				return $this->treatSwitch($pointer,$newchunks,$context);
				break;
			case ereg('^if(.+)not-equal(.+)$',$command,$found):
				// echo wakaffvar($found,'if{}equal');
				$operand1=$found[1];
				$operand2=$found[2];
				return $this->treatIfNotEqual($operand1,$operand2,$newchunks,$context);
				break;
			case ereg('^if(.+)equal(.+)$',$command,$found):
				// echo wakaffvar($found,'if{}equal');
				$operand1=$found[1];
				$operand2=$found[2];
				return $this->treatIfEqual($operand1,$operand2,$newchunks,$context);
				break;
			case ereg('^if-not(.*)$',$command,$found):
				$key=$found[1];
				return $this->treatIfNot($key,$newchunks,$context);
				break;
			case ereg('^if-notnull(.*)',$command,$found):
				$key=$found[1];
				return $this->treatIfNotNull($key,$newchunks,$context);
				break;
			case ereg('^if(.*)',$command,$found):
				$key=$found[1];
				return $this->treatIf($key,$newchunks,$context);
				break;
			default:
				// return '';
				return $this->treatChunks($newchunks);
		}
	}
	
	function treatForEach($path,&$chunks,$context=NULL) {
		//echo '<p>treating foreach '.$path.', '.wakaffvar($chunks,'chunks').'</p>';
		$array=$this->data->get($path);
		if (!is_array($array)) {
			trigger_error('templateFeederMagic: invalid __foreach{path}#id__ command, path '.$path.' is not an array');
			return NULL;
		}
		$keys=array_keys($array);
		//$keys=$data->getKeys($path);
		$treated='';
		foreach ($keys as $key) {
			// defining new context
			$newcontext=$context.str_replace('~','',$path).'['.$key.']';
			$this->data->setContext($newcontext);
			$copyChunks=$chunks;
			$treated.=$this->treatChunks($copyChunks,$newcontext);
		}
		$this->data->setContext($context);
		return $treated;
	}
	
	function treatSwitch($pointer,&$chunks,$context=NULL) {
		$switchValue=$this->getValue($pointer);
		// echo '<p>treating switch ('.wakaffvar($switchValue,$pointer).', '.wakaffvar($context,'context').'), '.wakaffvar($chunks,'chunks').'</p>';
		// $this->setSwitchValue($value);
		// parcours des chunks à la recherche des blocs case/default
		// evacuation des chunks ne correspondant pas
		$ereg_case='^__case([^# ]+)#(.+)__$';
		$ereg_default='^__default#(.+)__$';
		while ($chunk=array_shift($chunks)) {
			if (ereg($ereg_case,$chunk,$found)) {
				// bloc case
				// suppression de chunk parasite (cf regex de detection de commande)
				$this->waste[]=array_shift($chunks); //echo '<p>'.wakaffvar($waste,'waste').'</p>';
				// valeur de comparaison
				$casePointer=$found[1];
				$id=$found[2];
				$caseValue=$this->getValue($casePointer);
				//trigger_error("templatefeeder_magic: comparing $caseValue==$switchValue");
				if ($caseValue==$switchValue) {
					if (!$caseChunks=$this->extractChunksBlock($chunks,$id)) {
						return NULL;
						break;
					}
					// echo wakaffvar($caseChunks, 'case '.$caseValue);
					return $this->treatChunks($caseChunks);
				}
			} else if (ereg($ereg_default,$chunk,$found)) {
				// bloc default
				// suppression de chunk parasite (cf regex de detection de commande)
				$this->waste[]=array_shift($chunks).' in treatSwitch'; //echo '<p>'.wakaffvar($waste,'waste').'</p>';
				$id=$found[1];
				if (!$defaultChunks=$this->extractChunksBlock($chunks,$id)) {
					return NULL;
					break;
				}
				return $this->treatChunks($defaultChunks);
			}
			// on ignore les blocs non pertinents
		}
	}
	
	// function treatCase
	
	function treatIf($key,&$chunks,$context=NULL) {
		$value=$this->getValue($key); // echo wakaffvar($value,$key);
		// echo '<p>treating if(-notnull)? ('.wakaffvar($value,$key).', '.wakaffvar($context,'context').'), '.wakaffvar($chunks,'chunks').'</p>';
		if ($value) {
			return $this->treatChunks($chunks,$context);
		} else {
			return '';
		}
	}
	/**
	* processes if-notnull commands
	* @param string $key
	* @param array &$chunks
	* @param string $context opt
	* @access private
	* @return string
	*/
	function treatIfNotNull($key,&$chunks,$context=NULL) {
		$value=$this->getValue($key); // echo wakaffvar($value,$key);
		// echo '<p>treating if-notnull ('.wakaffvar($value,$key).', '.wakaffvar($context,'context').'), '.wakaffvar($chunks,'chunks').'</p>';
		if (!is_null($value)) {
			return $this->treatChunks($chunks,$context);
		} else {
			return '';
		}
	}
	
	function treatIfEqual($operand1,$operand2,&$chunks,$context=NULL) {
		$value1=$this->getValue($operand1);
		$value2=$this->getValue($operand2);
		if ($value1==$value2) {
			return $this->treatChunks($chunks,$context);
		} else {
			return '';
		}
	}
	
	function treatIfNotEqual($operand1,$operand2,&$chunks,$context=NULL) {
		$value1=$this->getValue($operand1);
		$value2=$this->getValue($operand2);
		if ($value1!=$value2) {
			return $this->treatChunks($chunks,$context);
		} else {
			return '';
		}
	}
	
	function treatIfNot($key,&$chunks,$context) {
		$value=$this->getValue($key); // echo wakaffvar($value,$key);
		if (!$value) {
			return $this->treatChunks($chunks,$context);
		} else {
			return '';
		}
	}
	
/*-------------------------------------------------------------------------------------------------
                                                                                           routines
-------------------------------------------------------------------------------------------------*/
	function getValue($key) {
		switch (true) {
			case ereg('^\((.*)\)$',$key,$found):
				return $found[1];
				break;
			case ereg('^~?\[.+\](key)?$',$key):
				return $this->getPathValue($key);
				break;
			case ereg('^[A-Z_]+$',$key):
				return $this->getConstant($key);
				break;
			default:
				return NULL;
		}
	}
	
	function getConstant($constant) {
		if (defined($constant))
			return constant($constant);
			
		return NULL;
	}
	
	function & getPathValue($path,$context=NULL) {
		$value=&$this->data->get($path);
		// if ($this->verbose) echo "<p>getting value for $path : ".wakaffvar($value)."</p>";
		if (is_array($value)) $value="Array!";
		else if (is_null($value)) $value=NULL;
		/*if (is_null($value)) {
			trigger_error($path.' (context is '.$this->data->getContext().') is null');
		}*/
		else $value=strval($value);
		return $value;
	}
	
	/*function fromLitteral($value) {
		switch (true) {
			case ($value=='true!'):
				return true;
			case ($value=='false!')
		}
	}*/


	// remplace un bloc (commande+contenu+fermeture)
	/*function & replaceBlock(& $template,$id,$replacement='') {
		//$command=$this->escapeEregChars($command);
		if (!ereg('__end'.$id.'__',$template)) {
			trigger_error('commande '.$id.' non fermée',E_USER_NOTICE);
			return false;
		}
		$ereg='__'.$this->ereg_command.$id.'__.*__end'.$id.'__';// echo $ereg;
		return $template=ereg_replace($ereg,$replacement,$template);
	}*/
	/*function & getBlock(&$template,$id) {
		$ereg='__'.$this->ereg_command.$id.'__(.*)__end'.$id.'__';
		if (!ereg($ereg,$template,$found)) return false;
		//echo "<p>getting block $id ".affvar($found)."<p>";
		return $found[2];
	}*/
	/*function & removeBlockCommand(& $template,$id) {
		$ereg_command='__'.$this->ereg_command.'('.$this->ereg_id.')?__';
		if (!$template=ereg_replace($ereg_command,'',$template)) return false;
		if (!ereg('__end'.$id.'__',$template)) return true;
		else return $template=ereg_replace('__end'.$id.'__','',$template);
	}*/
	function escapeEregChars($path) {
		$path=str_replace('[','\[',$path);
		$path=str_replace(']','\]',$path);
		$path=str_replace('(','\(',$path);
		$path=str_replace(')','\)',$path);
		//$path=str_replace('\\','\\\\',$path);
		return $path;
	}
	// renvoie la valeur textuelle d'une operande, valeur du chemin, constante ou (valeur)
	function getStrVal($operand) {
		//echo $operand;
		switch (true) {
			case ereg('^\((.*)\)$',$operand,$found): // string operand = (string)
				return $this->litteralValue($found[1]);
			case ereg($this->ereg_path,$operand): // path operand
				$value=$this->data->get($operand);// echo affvar($value)."=>".strval($value);
				return $this->litteralValue($value);
			case (ereg('^'.$this->ereg_constant.'$',$operand) and defined($operand)): // constant operand
				//$value=eval(" return $operand;");
				$value=constant($operand);
				$this->litteralValue($value);
			default:
				return $this->litteralValue($operand);
		}
	}
	
	function litteralValue($value) {
		switch (true) {
			case (is_string($value)):
				return $value;
			case (is_bool($value)): // boolean
				return $value?'true!':'false!';
			case (is_array($value)):
				return join(';',$value);
			case (is_null($value)):
				return 'null!';
			case is_numeric($value):
				return strval($value);
			default:
				return strval($value);
		}
	}
	
	/*function setSwitchValue($value) {
		$this->switchValues[]=$value;
	}
	
	function getSwitchValue($value) {
		return last($this->switchValues);
	}
	
	function delSwitchValue() {
		return pop($this->switchValues);
	}*/
	
	/* extrait tous les chunks d'un bloc et les retourne
	les supprime de la pile de chunks initiale ainsi que la commande de fin de bloc
	*/
	function & extractChunksBlock(&$chunks,$id) {
		$fullid='__end#'.$id.'__'; // echo $fullid;
		if (!$end=array_search($fullid,$chunks)) {
			trigger_error('templateFeederMagic: unclosed block command '.$id);
			//echo wakaffvar($chunks);
			return false;
		}
		// echo wakaffvar($end,'end');
		// extraction des chunks du bloc
		for ($f=0; $f<$end; $f++) {
			$newchunks[]=array_shift($chunks);
		}
		//suppression de la commande de fin de bloc
		// 2 fois, cf regex de decoupage en chunks
		$this->waste[]=array_shift($chunks).' in extractChunksBlock'; //echo '<p>'.wakaffvar($waste,'waste').'</p>';
		$this->waste[]=array_shift($chunks).' in extractChunksBlock'; // echo '<p>'.wakaffvar($waste,'waste').'</p>';
		
		return $newchunks;
	}
}
?>
